package cn.skyquant.quant4j.jforex.sdk.strategy2;

import cn.skyquant.quant4j.api.account.AccountDTO;
import cn.skyquant.quant4j.api.account.AccountService;
import cn.skyquant.quant4j.api.account.AccountServiceProxy;
import cn.skyquant.quant4j.api.account.AccountType;
import cn.skyquant.quant4j.api.dto.ResultDTO;
import cn.skyquant.quant4j.api.ea.EAManagerDTO;
import cn.skyquant.quant4j.api.ea.EAService;
import cn.skyquant.quant4j.api.ea.EAServiceProxy;
import cn.skyquant.quant4j.api.enums.BorkerType;
import cn.skyquant.quant4j.api.goods.GoodsDTO;
import cn.skyquant.quant4j.api.goods.GoodsService;
import cn.skyquant.quant4j.api.goods.GoodsServiceProxy;
import cn.skyquant.quant4j.api.strategy.StrategyParameter;
import cn.skyquant.quant4j.api.strategy.StrategyTestService;
import cn.skyquant.quant4j.api.strategy.StrategyTestServiceProxy;
import cn.skyquant.quant4j.jforex.sdk.strategy.MethodProcessManager;
import cn.skyquant.quant4j.jforex.sdk.strategy.SubmitOrder;
import cn.skyquant.quant4j.sdk.enums.Status;
import cn.skyquant.quant4j.sdk.util.time.CalendarUtils;
import com.dukascopy.api.*;

import java.io.PrintStream;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;

public abstract class QuantStrategy implements IStrategy {

    @Configurable("运行时间间隔")
    public Period period = Period.ONE_HOUR;
    @Configurable("EAID")
    public int eaId = 0;
    @Configurable("测试数据是否入库")
    public boolean testdata = false;
    @Configurable("测试名称")
    public String testname = "普通测试";

    /*JForex平台相关*/
    protected IEngine engine;
    protected IConsole console;
    protected IHistory history;
    protected IContext context;
    protected IIndicators indicators;
    protected IUserInterface userInterface;
    protected IDataService dataService;
    protected IAccount account;
    protected JFUtils jfUtils;
    private boolean isStart =false;
    public int r_avg_spread = 10;
    public int t_avg_spread = 20;

    /*业务相关*/
    protected AccountService accountService;
    private EAService eaService;
    private StrategyTestService testService;
    protected GoodsService goodsService;
    private EAManagerDTO eaManagerDTO;
    protected AccountDTO accountDTO;    //最重要的是account.id
    protected MethodProcessManager mpm;
    private Integer testId;
    protected String accountType = null;

    protected final void out(String format_test, String format_real, Object... args) {
        if (isTestMode()) {
            println(console.getOut(), format_test, args);
        } else {
            println(console.getOut(), format_real, args);
        }
    }

    protected final void err(String format_test, String format_real, Object... args) {
        if (isTestMode()) {
            println(console.getErr(), format_test, args);
        } else {
            println(console.getErr(), format_real, args);
        }
    }

    protected final void info(String format_test, String format_real, Object... args) {
        if (isTestMode()) {
            println(console.getInfo(), format_test, args);
        } else {
            println(console.getInfo(), format_real, args);
        }
    }

    protected final void notif(String format_test, String format_real, Object... args) {
        if (isTestMode()) {
            println(console.getNotif(), format_test, args);
        } else {
            println(console.getNotif(), format_real, args);
        }
    }

    protected final void warn(String format_test, String format_real, Object... args) {
        if (isTestMode()) {
            println(console.getWarn(), format_test, args);
        } else {
            println(console.getWarn(), format_real, args);
        }
    }

    protected final void println(PrintStream ps, String format, Object... args) {
        Formatter formatter = new Formatter();
        formatter.format(format, args);
        String formatStr = formatter.toString();
        ps.println(formatStr);
    }

    //是否测试模式
    protected final boolean isTestMode() {
        return engine.getRunMode() == IEngine.RunMode.LOCAL && engine.getType() == IEngine.Type.TEST;
    }

    //是否实盘模式
    protected final boolean isRealMode() {
        return engine.getRunMode() == IEngine.RunMode.LOCAL && engine.getType() == IEngine.Type.LIVE;
    }

    //是否模拟模式
    protected final boolean isDemoMode() {
        return engine.getRunMode() == IEngine.RunMode.LOCAL && engine.getType() == IEngine.Type.DEMO;
    }

    @Override
    public final void onStart(IContext context) throws JFException {
        this.engine = context.getEngine();
        this.console = context.getConsole();
        this.history = context.getHistory();
        this.context = context;
        this.indicators = context.getIndicators();
        this.userInterface = context.getUserInterface();
        this.dataService = context.getDataService();
        this.jfUtils = context.getUtils();
        this.account = context.getAccount();
        String accountCode = account.getAccountId();
        goodsService = new GoodsServiceProxy(accountCode, BorkerType.DUKASCOPY);
        eaService = new EAServiceProxy(accountCode,BorkerType.DUKASCOPY);
        accountService = new AccountServiceProxy(accountCode,BorkerType.DUKASCOPY);
        IWLabelData whiteLabelData = dataService.getWhiteLabelData();
        String accountName = account.getUserName();
        String borkerName = whiteLabelData.getLongName();
        String currency = account.getAccountCurrency().getSymbol();
        Set<IClientInfo> clientInfoSet = account.getClients();



        switch (engine.getType()){
            case DEMO:accountType = "DEMO";break;
            case LIVE:accountType = "LIVE";break;
        }
        if(clientInfoSet.size()>1){
            accountType = AccountType.PAMM.name();
        }

        //首先要看一下是否有account，如果没有，则注册一个
        accountDTO = createOrUpdateAccount(accountCode, accountType, accountName, borkerName, currency, null);
        //注册子帐号
        if(clientInfoSet!=null && !clientInfoSet.isEmpty()){
            for(IClientInfo clientInfo:clientInfoSet){
                if(clientInfo.getClientId().equals(account.getAccountId())){
                    continue;
                }
                String client_code = clientInfo.getClientId();
                String client_currency = clientInfo.getAccountCurrency().getSymbol();
                String client_type = AccountType.CLIENT.name();
                createOrUpdateAccount(client_code, client_type, null, borkerName, client_currency, accountCode);
            }
        }

        if (accountDTO == null) {
            err("init account error","初始化账户数据错误");
            context.stop();
            return;
        }

        //第二步，检测EA的状态（只在真实环境中才需要检测）
        eaManagerDTO = null;
        if (isRealMode() || isDemoMode()) {
            ResultDTO<EAManagerDTO> eaDTO = eaService.getOrCreate(getName(),eaId,accountDTO.id);
            if(eaDTO.okData() && eaDTO.data.eaStatus== Status.A){
                eaManagerDTO = eaDTO.data;
                eaService.ready(eaManagerDTO.id,null,null,null);
                return;
            }else {
                context.stop();
                err("account[%s %s],ea[%s-%d]:no authorize:%s,quit!",
                        "账户[%s %s],策略[%s-%d] 无授权,不能运行:%s,退出!",
                        accountCode,account.getUserName(),getName(),eaId,eaDTO.message);
            }
        }
        out("ea:%s,version:%s, id:%d-%d, starting...", "策略:%s,版本:%s,id:%d-%d 启动中...", getName(), getVersion(),eaId,eaManagerDTO.id);
        isStart = onStartEx();
        if (eaManagerDTO!=null) {
            eaService.start(eaManagerDTO.id);
        }
        if (isTestMode() && testdata) {
            testService = new StrategyTestServiceProxy(account.getAccountId(), BorkerType.DUKASCOPY);
            List<StrategyParameter> parameterList = new ArrayList();
            Class c = this.getClass();
            Field[] fields = c.getFields();
            for (Field field : fields) {
                Configurable config = field.getAnnotation(Configurable.class);
                if (config != null) {
                    String desc = config.value();
                    if (desc != null) {
                        try {
                            Object obj = field.get(this);
                            if (obj != null) {
                                String value = obj.toString();
                                String name = field.getName();
                                StrategyParameter strategyParameter = new StrategyParameter(name, desc, value);
                                parameterList.add(strategyParameter);
                            }
                        } catch (Exception e) {
                        }
                    }
                }
            }
            String parameter = StrategyParameter.toJSONString(parameterList);
            ResultDTO<Integer> resultDTO = testService.start(getName(), String.valueOf(getVersion()), testname, null, context.getTime(), parameter, account.getEquity());
            last_testdata_time = context.getTime();
            daily_open = account.getEquity();
            if (resultDTO.okData()) {
                testId = resultDTO.data;
            } else {
                err("error reason:%s", "错误原因:%s", resultDTO.message);
            }
        }
        if (isStart) {
            info("account:[%s %s],ea:[%s %s] start successfully.^_^",
                    "账户:[%s %s],策略:[%s %s] 启动成功.^_^",
                    account.getAccountId(), account.getUserName(), getName(), getVersion());
        } else {
            err("account:[%s %s],ea:[%s %s] start fail.T_T",
                    "账户:[%s %s],策略:[%s %s] 启动失败.T_T",
                    account.getAccountId(), account.getUserName(), getName(), getVersion());
            context.stop();
        }
        updateAccountInfo(account);//更新账户资产信息
    }

    //创建或更新帐号信息
    private AccountDTO createOrUpdateAccount(String accountCode,
                                             String accountType,
                                             String accountName,
                                             String borkerName,
                                             String currency,
                                             String parentCode) {
        ResultDTO<AccountDTO> accountDTOResultDTO = accountService.get(accountCode, BorkerType.DUKASCOPY);
        if (accountDTOResultDTO.okData()) {
            AccountDTO accountDTO = accountDTOResultDTO.data;
            if (accountDTO.type == null && accountType != null) {
                //如果已经有了,但是type不对的话，要更新type
                accountService.updateType(accountDTO.id, accountType);
            }
        } else {
            //如果account没有，则创造一个type
            accountDTOResultDTO = accountService.create(accountCode, BorkerType.DUKASCOPY, accountType, accountName, borkerName, currency, parentCode);
        }
        return accountDTOResultDTO.data();
    }

    @Override
    public final void onStop() throws JFException {
        if (isStart) {
            onStopEx();
            if (eaManagerDTO != null) {
                eaService.stop(eaManagerDTO.id);
            }
        }
        if (testdata && isTestMode() && testId != null) {
            testService.end(testId, context.getTime(), account.getEquity());
            testId = null;
        }
        out("%s %s stop",
                "%s %s 退出",
                getName(), getVersion());
        isStart = false;
        eaService = null;
        testService = null;
        eaManagerDTO = null;
        engine = null;
        console = null;
        history = null;
        context = null;
        indicators = null;
        userInterface = null;
        dataService = null;
        account = null;
        jfUtils = null;
    }

    //更新账户余额和净资产
    protected void updateAccountInfo(IAccount account){
        String accountCode = account.getAccountId();
        double balance = new BigDecimal(account.getBalance()).setScale(2, RoundingMode.HALF_UP).doubleValue();
        double equity = new BigDecimal(account.getEquity()).setScale(2, RoundingMode.HALF_UP).doubleValue();
        double usedMargin = new BigDecimal(account.getUsedMargin()).setScale(2, RoundingMode.HALF_UP).doubleValue();
        double used_r = usedMargin/equity;
        accountService.update(accountCode,BorkerType.DUKASCOPY,balance,equity,usedMargin,context.getTime());

        Set<IClientInfo> clientInfos = account.getClients();
        for(IClientInfo clientInfo:clientInfos){
            if(clientInfo.getClientId().equals(account.getAccountId())){
                continue;
            }
            double c_balance = new BigDecimal(clientInfo.getBalance()).setScale(2, RoundingMode.HALF_UP).doubleValue();
            double c_equity = new BigDecimal(clientInfo.getEquity()).setScale(2, RoundingMode.HALF_UP).doubleValue();
            double c_used_margin = new BigDecimal(c_equity * used_r).setScale(2, RoundingMode.HALF_UP).doubleValue();
            accountService.update(clientInfo.getClientId(),BorkerType.DUKASCOPY,c_balance,c_equity,c_used_margin,context.getTime());
        }
    }

    @Override
    public final void onTick(Instrument instrument, ITick tick) throws JFException {
        if (!isStart) return;
        onTickEx(instrument, tick);
    }

    private long last_heartbeat_time = 0;//记录心跳的时间
    private long last_testdata_time;//记录测试数据的时间
    private Double daily_open;
    private Double daily_close;
    private Double daily_high;
    private Double daily_low;

    @Override
    public final void onBar(Instrument instrument, Period period, IBar askBar, IBar bidBar) throws JFException {
        if (!isStart) return;
        //5分钟记录1次心跳
        if (askBar.getTime() - last_heartbeat_time > Period.FIVE_MINS.getInterval() && eaManagerDTO!=null) {
            String date = CalendarUtils.formatTightStard(context.getTime());
            last_heartbeat_time = askBar.getTime();
            eaService.heartbeat(eaManagerDTO.id, date, null);
        }
        if (testdata && isTestMode() && testId != null && askBar.getTime() - last_testdata_time > Period.DAILY.getInterval()) {
            last_testdata_time = askBar.getTime();
            daily_close = account.getEquity();
            if (daily_high != null && daily_low != null) {
                testService.updateDaily(testId, last_testdata_time, account.getBalance(), account.getEquity(), daily_open, daily_close, daily_high, daily_low);
            }
            daily_open = account.getEquity();
            daily_high = null;
            daily_low = null;
        }
        onBarEx(instrument, period, askBar, bidBar);
    }


    @Override
    public final void onMessage(IMessage message) throws JFException {
        if (!isStart) return;
        if (testdata && isTestMode() && testId != null) {
            IOrder order = message.getOrder();
            if (order == null) {
                return;
            }
            switch (message.getType()) {
                case ORDER_FILL_OK:
                    testService.addOpen(testId, message.getCreationTime(), order.getInstrument().name(), order.getOpenPrice(), order.getAmount(), order.getOrderCommand().name(), order.getStopLossPrice(), order.getTakeProfitPrice());
                    break;
                case ORDER_CLOSE_OK:
                    if (order.getState() == IOrder.State.CLOSED &&
                            order.getProfitLossInUSD() != 0) {
                        testService.addClose(testId, message.getCreationTime(), order.getInstrument().name(), order.getOpenPrice(), order.getClosePrice(), order.getAmount(), order.getOrderCommand().name(), order.getProfitLossInUSD(), order.getCommissionInUSD());
                    }
                    break;
            }
        }
        onMessageEx(message);
    }

    protected final void addStatistics(long beginTime, long endTime, double beginMoney, double endMoney, int tradeLevel, double profit, double amount, int distance) {
        if (testdata && isTestMode() && testId != null) {
            testService.addStatistics(testId, beginTime, endTime, beginMoney, account.getEquity(), tradeLevel, profit, amount, distance);
        }
    }


    @Override
    public final void onAccount(IAccount account) throws JFException {
        if (!isStart) return;
        if (testdata && isTestMode() && testId != null) {
            double equity = account.getEquity();
            if (daily_high == null || equity > daily_high) {
                daily_high = equity;
            }
            if (daily_low == null || equity < daily_low) {
                daily_low = equity;
            }
        }
        onAccountEx(account);
    }

    //批量安全提交订单,带点差判断
    protected void safeBatchSubmitOrder(List<SubmitOrder> orderList) {
        boolean safe = true;
        try {
            for (SubmitOrder order : orderList) {
                if (isSafe(order.instrument, order.orderCommand, order.price)) {
                    safe = false;
                    break;
                }
            }
        } catch (JFException e) {
            safe = false;
        }
        if (safe) {
            try {
                for (SubmitOrder order : orderList) {
                    engine.submitOrder(order.label, order.instrument, order.orderCommand, order.amount, order.price, order.slippage, order.stopLossPrice, order.takeProfitPrice, order.goodTillTime, order.comment);
                }
            } catch (JFException e) {

            }
        }
    }

    //安全提交订单,带点差判断
    protected IOrder safeSubmitOrder(String label, Instrument instrument, IEngine.OrderCommand orderCommand, double amount, double price, double slippage, double stopLossPrice, double takeProfitPrice, long goodTillTime, String comment) {
        IOrder order = null;
        try {
            if (isSafe(instrument, orderCommand, price)) {
                order = engine.submitOrder(label, instrument, orderCommand, amount, price, slippage, stopLossPrice, takeProfitPrice, goodTillTime, comment);
            } else {
                out("%s spread is to large", "%s当前点差太大，不能交易", instrument.name());
            }
        } catch (JFException e) {
            err("submit order error:%s","提交订单错误:%s", e.getMessage());
        }
        return order;
    }

    private boolean isSafe(Instrument instrument, IEngine.OrderCommand orderCommand, double price) throws JFException {
        ITick tick = history.getLastTick(instrument);
        switch (orderCommand) {
            case BUY:
            case SELL:
                ResultDTO<GoodsDTO> goodsDTOResultDTO = goodsService.get(instrument.name(), BorkerType.DUKASCOPY);
                if (goodsDTOResultDTO.okData()) {
                    GoodsDTO goodsDTO = goodsDTOResultDTO.data;
                    double avgSpread = goodsDTO.avgSpread;
                    double diff = tick.getAsk() - tick.getBid();
                    double x = isTestMode() ? t_avg_spread : r_avg_spread;
                    if (diff < avgSpread * x) {
                        return true;
                    }
                }
                break;
            case BUYLIMIT:
                if (price < tick.getBid()) {
                    return true;
                }
                break;
            case SELLLIMIT:
                if (price > tick.getAsk()) {
                    return true;
                }
                break;
            case BUYSTOP:
                if (price > tick.getAsk()) {
                    return true;
                }
                break;
            case SELLSTOP:
                if (price < tick.getBid()) {
                    return true;
                }
                break;
        }
        return false;
    }

    /*需要子类实现的方法*/
    protected abstract String getVersion();
    protected abstract String getName();
    protected abstract boolean onStartEx();
    protected abstract void onTickEx(Instrument instrument, ITick tick);
    protected abstract void onBarEx(Instrument instrument, Period period, IBar askBar, IBar bidBar);
    protected abstract void onMessageEx(IMessage message);
    protected abstract void onAccountEx(IAccount account);
    protected abstract void onStopEx();

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        QuantStrategy that = (QuantStrategy) o;
        return eaId == that.eaId && getName().equals(that.getName());
    }

    @Override
    public int hashCode() {
        return Objects.hash(eaId);
    }
}
